package mviewer.model3dLoader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import mviewer.types.IModel3d;
import mviewer.types.Model3d;
import mviewer.types.IModel3dBuilder;
import mviewer.types.Material;
import mviewer.types.Model3dComposite;

public class ObjModelLoader implements IModel3dLoader {

	static private final Logger logger = Logger.getLogger(ObjModelLoader.class.getName());

	static private final String SCENE_ROOT = "scene-root3d";

	static private class ModelLoaderData {

		public IModel3dBuilder model;
		public int vertexOffset, textureCoordinateOffset;

		public ModelLoaderData(IModel3dBuilder model, int vertexOffset, int textureCoordinateOffset) {
			this.model = model;
			this.vertexOffset = vertexOffset;
			this.textureCoordinateOffset = textureCoordinateOffset;
		}
	}

	private Map<String, ITokenParser> parser;
	private Model3dComposite model;
	private Map<String, ModelLoaderData> parts;
	private ModelLoaderData current;
	private String workingDirectory;
	private int vertexCount, textureCoordinateCount;
	private MtlLibLoader mtlLibLoader;
	private Map<String, Material> materials;
	private ITokenParser nameParser, compositeChildParser;
	private Material currentMaterial, defaultMaterial;

	public ObjModelLoader() {
		nameParser = new ModelNameParser();
		compositeChildParser = new ObjectInstantiationParser();
		mtlLibLoader = new MtlLibLoader();
		parser = new HashMap<String, ITokenParser>();

		parser.put("mtllib", new MtlIncludingParser());
		parser.put("v", new VertexParser());
		parser.put("vt", new TextureCoordinateParser());
		parser.put("vn", new VertexNormalParser());
		parser.put("f", new FaceParser());
		parser.put("usemtl", new UseMtlParser());
		parser.put("s", new SmoothParser());
		parser.put("g", new GroupParser());
	}

	@Override
	public boolean supports(String file) {
		return file.endsWith(".obj");
	}

	@Override
	public IModel3d loadModel(String file)
			throws FileNotFoundException, IOException {
		currentMaterial = defaultMaterial = new Material();
		// read obj format
		parser.put("o", nameParser);
		int lastSeparator = file.lastIndexOf(File.separator) + 1;
		workingDirectory = file.substring(0, lastSeparator);
		model = new Model3dComposite();
		current = new ModelLoaderData(model, 0, 0);
		parts = new HashMap<String, ModelLoaderData>();
		parts.put(SCENE_ROOT, current);
		vertexCount = textureCoordinateCount = 0;
		materials = null;
		BufferedReader reader = new BufferedReader(new FileReader(file));
		String line;
		String[] token;
		String[] dataToken;
		while (reader.ready()) {
			line = reader.readLine();
			if (!line.trim().isEmpty()) {
				token = line.split(" ");
				if (!token[0].equals("#")) {
					dataToken = Arrays.copyOfRange(token, 1, token.length);
					try {
						parser.get(token[0]).parse(dataToken);
					} catch(NullPointerException e) {
						throw new IllegalArgumentException("Unknown OBJ file element: " + token[0], e);
					}
				}
			}
		}
		reader.close();
		for (ModelLoaderData d : parts.values())
			d.model.apply();
		return model;
	}

	private String createUniqueName(String name) {
		return parts.containsKey(name)
				? createUniqueName(name + "_") : name;
	}

	interface ITokenParser {
		public void parse(String[] token) throws IOException;
	}

	private class ObjectInstantiationParser implements ITokenParser {
		public void parse(String[] token) {
			String modelName = token[0];
			current = parts.get(modelName);
			if (current == null) {
				modelName = createUniqueName(modelName);
				current = new ModelLoaderData(new Model3d(modelName), vertexCount, textureCoordinateCount);

				parts.put(modelName, current);
				model.getChildren().add((IModel3d)current.model);

				current.model.setMaterial(currentMaterial);
			}
		}
	}

	private class ModelNameParser implements ITokenParser {
		public void parse(String[] token) {
			String modelName = token[0];
			current.model.setName(modelName);
			parts.remove(SCENE_ROOT);
			parts.put(modelName, current);
			parser.put("o", compositeChildParser);
		}
	}

	private class GroupParser implements ITokenParser {
		public void parse(String[] token) { }
	}

	private class MtlIncludingParser implements ITokenParser {
		public void parse(String[] token) throws IOException {
			String mtlFile = token[0].charAt(0) == File.separatorChar ? token[0]
								: workingDirectory + token[0];
			try {
				materials = mtlLibLoader.load(mtlFile);
			} catch(IllegalArgumentException e) {
				logger.log(Level.WARNING, "invalid mtllib", e);
			} catch(Exception e) {
				logger.warning(String.format("Couldn't load material library: %s", mtlFile));
			}
		}
	}

	private class UseMtlParser implements ITokenParser {
		public void parse(String[] token) {
			if (materials != null) {
				String material = token[0];
				currentMaterial = materials.get(material);

				if (currentMaterial == null) {
					logger.warning(String.format("Undefined material: %s", material));
					currentMaterial = defaultMaterial;
				}

				current.model.setMaterial(currentMaterial);
			}
		}
	}

	private class SmoothParser implements ITokenParser {
		public void parse(String[] token) {
			current.model.getMaterial().setSmooth(!token[0].equals("off"));
		}
	}

	private class TextureCoordinateParser implements ITokenParser {
		public void parse(String[] token) {
			try {
				float u = Float.valueOf(token[0]),
					  v = 1 - Float.valueOf(token[1]);
				current.model.appendTextureCoordinate(u, v);
				textureCoordinateCount++;
			} catch(NumberFormatException e) {
				throw new IllegalArgumentException("Word found but texture coordinate expected: " + Arrays.toString(token));
			}
		}
	}

	private class VertexNormalParser implements ITokenParser {
		public void parse(String[] token) { }
	}

	private class VertexParser implements ITokenParser {

		public void parse(String[] token) throws IOException {
			if (current == null)
				parser.get("o").parse(new String[] {"unknown"});
			if (token.length!=3)
				throw new IllegalArgumentException("More than 3 vertex coordinates given: " + Arrays.toString(token));
			try {
				current.model.appendVertex(Float.valueOf(token[0]),
										 Float.valueOf(token[1]),
										 Float.valueOf(token[2]));
				vertexCount++;
			} catch(NumberFormatException e) {
				throw new IllegalArgumentException("Word found but vertex coordinate expected: " + Arrays.toString(token));
			}
		}
	}

	private class FaceParser implements ITokenParser {

		public void parse(String[] token) throws IOException {
			int[] vertIndices = new int[token.length];
			int[] textureIndices = new int[token.length];
			boolean textureCoordinatesFound = false;
			String[] subToken;
			for (int i = 0; i < token.length; i++) {
				try {
					subToken = token[i].split("/");
					vertIndices[i] = Integer.valueOf(subToken[0]) - current.vertexOffset - 1;
					if (subToken.length > 1 && !subToken[1].isEmpty()) {
						int tci = Integer.valueOf(subToken[1]);
						if (tci > 0) {
							textureIndices[i] = tci - current.textureCoordinateOffset - 1;
							textureCoordinatesFound = true;
						}
					}
				} catch(NumberFormatException e) {
					throw new IllegalArgumentException("Unsupported face format: " + token[i]);
				}
			}
			if (textureCoordinatesFound)
				current.model.appendPolygon(vertIndices, textureIndices);
			else
				current.model.appendPolygon(vertIndices);
		}
	}
}
